﻿-- ********************************************************************************
-- Lazy Transform
-- Automates the process of milling, prospecting and disenchanting.
-- By: Shenton
--
-- Core.lua
-- ********************************************************************************

-- Ace libs (<3)
local A = LibStub("AceAddon-3.0"):NewAddon("LazyTranform", "AceConsole-3.0", "AceEvent-3.0", "AceTimer-3.0");
local L = LibStub("AceLocale-3.0"):GetLocale("LazyTransform", false);

-- Addon global
_G["LazyTransformGlobal"] = A;
A.L = L;

-- LibDBIcon
A.LibDBIcon = LibStub("LibDBIcon-1.0");

-- ********************************************************************************
-- Global to local
-- ********************************************************************************

-- LUA
local pairs = pairs;
local string = string;
local tostring = tostring;

-- WoW
local DEFAULT_CHAT_FRAME = DEFAULT_CHAT_FRAME;
local IsShiftKeyDown = IsShiftKeyDown;
local UIParent = UIParent;
local GetItemInfo = GetItemInfo;
local GetProfessionInfo = GetProfessionInfo;
local tContains = tContains;
local GetSpellInfo = GetSpellInfo;
local GetBagName = GetBagName;
local GetContainerNumSlots = GetContainerNumSlots;
local GetContainerItemID = GetContainerItemID;
local GetContainerItemInfo = GetContainerItemInfo;
local GetProfessions = GetProfessions;
local InCombatLockdown = InCombatLockdown;
local GetItemCount = GetItemCount;
local GetBindingKey = GetBindingKey;
local SetOverrideBindingClick = SetOverrideBindingClick;
local GameTooltip = GameTooltip;
local NUM_BAG_FRAMES = NUM_BAG_FRAMES;
local PlaySound = PlaySound;
local ITEM_BIND_ON_EQUIP = ITEM_BIND_ON_EQUIP;
local IsControlKeyDown = IsControlKeyDown;
local UnitCastingInfo = UnitCastingInfo;
local UnitChannelInfo = UnitChannelInfo;
local GetMerchantItemInfo = GetMerchantItemInfo;
local GetScreenWidth = GetScreenWidth;

-- Others
local LibStub = LibStub;
local LazyTransformSecureButton = LazyTransformSecureButton;
local LazyTransformSecureButtonHotKey = LazyTransformSecureButtonHotKey;

-- ********************************************************************************
-- Variables
-- ********************************************************************************

-- AddOn version
A.version = GetAddOnMetadata("LazyTransform", "Version");

-- Text colors
A.color =
{
    RED = "|cffff3333";
    GREEN = "|cff33ff99";
    WHITE = "|cffffffff";
    WARRIOR = "|cffc79c6e";
    RESET = "|r";
};

-- Binding locale
BINDING_HEADER_LAZYTRANSFORM = L["Lazy Transform"];
BINDING_NAME_LAZYTRANSFORMBIND = L["Process"];

-- Databases
-- [itemid] = required rank
A.herbs =
{
    -- Mist of Pandaria
    [79011] = 500, -- Fool's Cap
    [79010] = 500, -- Snow Lily
    [72235] = 500, -- Silkweed
    [72234] = 500, -- Green Tea Leaf
    [72237] = 500, -- Rain Poppy
    [89639] = 500, -- Desecrated Herb
    -- Cataclysm
    [52987] = 475, -- Twilight Jasmine
    [52988] = 475, -- Whiptail
    [52986] = 450, -- Heartblossom
    [52984] = 425, -- Stormvine
    [52985] = 450, -- Azshara's Veil
    [52983] = 425, -- Cinderbloom
    -- Wrath of the Lich King
    [36905] = 325, -- Lichbloom
    [36906] = 325, -- Icethorn
    [36903] = 325, -- Adder's Tongue
    [39970] = 325, -- Fire Leaf
    [37921] = 325, -- Deadnettle
    [36901] = 325, -- Goldclover
    [36904] = 325, -- Tiger Lily
    [36907] = 325, -- Talandra's Rose
    -- Burning Crusade
    [22791] = 275, -- Netherbloom
    [22792] = 275, -- Nightmare Vine
    [22793] = 275, -- Mana Thistle
    [22790] = 275, -- Ancient Lichen
    [22787] = 275, -- Ragveil
    [22789] = 275, -- Terocone
    [22786] = 275, -- Dreaming Glory
    [22785] = 275, -- Felweed
    -- Vanilla
    [13467] = 200, --  Icecap
    [13466] = 225, -- Sorrowmoss
    [13465] = 225, -- Mountain Silversage
    [13463] = 225, -- Dreamfoil
    [13464] = 225, -- Golden Sansam
    [8846] = 175, -- Gromsblood
    [8845] = 175, -- Ghost Mushroom
    [8839] = 175, -- Blindweed
    [8838] = 175, -- Sungrass
    [8836] = 175, -- Arthas' Tears
    [8831] = 175, -- Purple Lotus
    [4625] = 175, -- Firebloom
    [3819] = 125, -- Dragon's Teeth
    [3358] = 125, -- Khadgar's Whisker
    [3821] = 125, -- Goldthorn
    [3818] = 125, -- Fadeleaf
    [3357] = 75, -- Liferoot
    [3356] = 75, -- Kingsblood
    [3355] = 75, -- Wild Steelbloom
    [3369] = 75, -- Grave Moss
    [2453] = 25, -- Bruiseweed
    [3820] = 25, -- Stranglekelp
    [2450] = 25, -- Briarthorn
    [2452] = 25, -- Swiftthistle
    [2449] = 1, -- Earthroot
    [785] = 1, -- Mageroyal
    [765] = 1, -- Silverleaf
    [2447] = 1, -- Peacebloom
};

A.ores =
{
    -- Mist of Pandaria
    [72094] = 600, -- Black Trillium Ore
    [72103] = 600, -- White Trillium Ore
    [72092] = 500, -- Ghost Iron Ore
    -- Cataclysm
    [52183] = 500, -- Pyrite Ore
    --[52184] = 425, -- Black Iron Ore - Not in game
    [52185] = 475, -- Elementium Ore
    [53038] = 425, -- Obsidium Ore
    -- Wrath of the Lich King
    [36910] = 450, -- Titanium Ore
    [36912] = 400, -- Saronite Ore
    [36909] = 350, -- Cobalt Ore
    -- Burning Crusade
    [23425] = 325, -- Adamantite Ore
    [23424] = 275, -- Fel Iron Ore
    -- Vanilla
    [3858] = 175, -- Mithril Ore
    [10620] = 250, -- Thorium Ore
    [2772] = 125, -- Iron Ore
    [2771] = 50, -- Tin Ore
    [2770] = 1, -- Copper Ore
};

A.rareOres =
{
    -- Mist of Pandaria
    72094, -- Black Trillium Ore
    72103, -- White Trillium Ore
    -- Cataclysm
    52183, -- Pyrite Ore
    -- Wrath of the Lich King
    36910, -- Titanium Ore
};

-- Disenchant required level table
-- A.disenchantReqLvl =
-- {
    -- ["2"] = -- Uncommon
    -- {
        -- ["1"] = 20,
        -- ["25"] = 25,
        -- ["50"] = 30,
        -- ["75"] = 35,
        -- ["100"] = 40,
        -- ["125"] = 45,
        -- ["150"] = 50,
        -- ["175"] = 55,
        -- ["200"] = 60,
        -- ["225"] = 99,
        -- ["275"] = 120,
        -- ["325"] = 150,
        -- ["350"] = 182,
        -- ["425"] = 232,
        -- ["1"] = 20,
    -- },
-- };

-- ********************************************************************************
-- Functions
-- ********************************************************************************

--- Send a message to the chat frame with the addon name colored
-- @param text The message to display
-- @param color Bool, if true will color in red
-- @param silent Bool if true will not play the sound
function A:Message(text, color, silent)
    if ( color ) then
        color = A.color["RED"];
    else
        color = A.color["GREEN"]
    end

    if ( not silent ) then
        PlaySound("TellMessage");
    end

    DEFAULT_CHAT_FRAME:AddMessage(color..L["Lazy Transform"]..": "..A.color["RESET"]..text);
end

--- Return the position of a value in a table
-- @param t The table
-- @param i The value
-- @return The value position or nil
function A:GetItemPos(t, i)
    local index = 1;

    for k,v in pairs(t) do
        if ( v == i ) then return index; end

        index = index + 1;
    end

    return nil;
end

--- Show or hide the minimap icon
function A:ShowHideMinimap()
    if ( A.db.profile.minimap.hide ) then
        A:Message(L["Minimap icon is hidden, use /lt minimap show or /lazytransform minimap show to display it again."], true);
        A.LibDBIcon:Hide("LazyTransform");
    else
        A.LibDBIcon:Show("LazyTransform");
    end
end

--- Toggle minimap visibility
function A:ToggleMinimapIcon()
    A.db.profile.minimap.hide = not A.db.profile.minimap.hide;

    A:ShowHideMinimap();
end

--- Command usage
function A:CommandUsage()
    A:Message(L["/lt or /lazytransform"], nil, 1);
    A:Message(L["With no argument it will display the configuration panel."], nil, 1);
    A:Message(L["/lt minimap <show||hide||toggle> - Minimap icon visibility."], nil, 1);
    A:Message(L["/lt button <show||hide||toggle> - Process button visibility"], nil, 1);
    A:Message(L["/lt button <lock||unlock||toggle2> - Process button lock/unlock state"], nil, 1);
    A:Message(L["/lt button reset - Reset process button position to the center of the screen."]);
end

--- Handle the slash command
-- @param input The string returned after the command
function A:SlashCommand(input)
    local arg1, arg2 = string.match(input, "(%a*)%s?(.*)");

    if ( arg1 == "" ) then
        LibStub("AceConfigDialog-3.0"):Open("LazyTransformConfig");
    elseif ( arg1 == "minimap" and arg2 == "show" ) then
        A.db.profile.minimap.hide = nil;
        A:ShowHideMinimap();
    elseif ( arg1 == "minimap" and arg2 == "hide" ) then
        A.db.profile.minimap.hide = 1;
        A:ShowHideMinimap();
    elseif ( arg1 == "minimap" and arg2 == "toggle" ) then
        A:ToggleMinimapIcon();
    elseif ( arg1 == "button" and arg2 == "show" ) then
        LazyTransformSecureButton:Show();
        A.db.profile.button.hide = nil;
    elseif ( arg1 == "button" and arg2 == "hide" ) then
        LazyTransformSecureButton:Hide();
        A.db.profile.button.hide = 1;
    elseif ( arg1 == "button" and arg2 == "toggle" ) then
        A:ToggleButtonHideShow();
    elseif ( arg1 == "button" and arg2 == "lock" ) then
        A:LockButton();
    elseif ( arg1 == "button" and arg2 == "unlock" ) then
        A:UnlockButton()
    elseif ( arg1 == "button" and arg2 == "toggle2" ) then
        A:ToggleButtonLock();
    elseif ( arg1 == "button" and arg2 == "reset" ) then
        A.db.profile.button.point.point = "CENTER";
        A.db.profile.button.point.relativeTo = UIParent;
        A.db.profile.button.point.relativePoint = "CENTER";
        A.db.profile.button.point.offX = 0;
        A.db.profile.button.point.offY = 0;

        A:SetButtonPos();
    else
        A:CommandUsage();
    end
end

--- Set LDB and LDB icon
function A:LDBInit()
    -- Already loaded
    if ( LibStub("LibDataBroker-1.1"):GetDataObjectByName("Lazy Transform") ) then return; end

    -- LDB
    A.ldb = LibStub("LibDataBroker-1.1"):NewDataObject("Lazy Transform",
    {
        type = "launcher",
        label = L["Lazy Transform"],
        icon = "Interface\\ICONS\\Icon_PetFamily_Mechanical",
        tocname = "LazyTransform",
        OnClick = function(self, button)
            if (button == "LeftButton") then
                if ( IsShiftKeyDown() ) then
                    A:ToggleButtonLock();
                else
                    A:ToggleButtonHideShow();
                end
            elseif ( button == "RightButton" ) then
                -- Menu
            elseif ( button == "MiddleButton" ) then
                LibStub("AceConfigDialog-3.0"):Open("LazyTransformConfig");
            end
        end,
        OnTooltipShow = function(tooltip)
            tooltip:AddDoubleLine(A.color["WHITE"]..L["Lazy Transform"], A.color["GREEN"].." v"..A.version);
            tooltip:AddLine(" ");
            tooltip:AddLine(L["|cFFC79C6ELeft-Click: |cFF33FF99Toggle button visibility.\n|cFFC79C6EShift+Left-Click: |cFF33FF99Toggle button lock.\n|cFFC79C6ERight-Click: |cFF33FF99Display quick access menu.\n|cFFC79C6EMiddle-Click: |cFF33FF99Open configuration panel."]);
        end,
    });

    -- LDBIcon
    A.LibDBIcon:Register("LazyTransform", A.ldb, A.db.profile.minimap);
end

--- Build the herbs and ores cache
function A:GetItemCache(item)
    local itemName = GetItemInfo(item);

    if ( not itemName ) then
        A:ScheduleTimer("GetItemCache", 1, item);
    end
end
function A:BuildItemsCache()
    for k,v in pairs(A.herbs) do
        A:GetItemCache(k);
    end
    for k,v in pairs(A.ores) do
        A:GetItemCache(k);
    end
end

--- Get professions informations and store them if needed
-- Archaeology (794)
-- Alchemy (171)
-- Blacksmith (164)
-- Cooking (184)
-- Enchanting (333)
-- Engineer (202)
-- First Aid (129)
-- Fishing (356)
-- Herbalism (182)
-- Inscription (773)
-- Jewelcrafting (755)
-- Leatherworking (165)
-- Mining (186)
-- Skinning (393)
-- Tailoring (197)
local workingProfessions =
{
    333,
    773,
    755,
};
local skillLineToName =
{
    [333] = "ENCHANTING",
    [773] = "INSCRIPTION",
    [755] = "JEWELCRAFTING",
};
local skillLineToSpellID =
{
    [333] = 13262,
    [773] = 51005,
    [755] = 31252,
};
function A:GetProfession(index)
    if ( index ) then
        local name, texture, rank, _, _, _, skillLine, rankModifier = GetProfessionInfo(index);

        if ( tContains(workingProfessions, skillLine) ) then
            local spellID = skillLineToSpellID[skillLine];
            local spellName, _, spellTexture = GetSpellInfo(spellID);

            return skillLineToName[skillLine], name, texture, rank, rankModifier, spellID, spellName, spellTexture, skillLine;
        end
    end

    return nil;
end
function A:GetProfessions()
    local prof1, prof2 = GetProfessions();
    local nameGlobal, name, texture, rank, rankModifier, spellID, spellName, spellTexture, skillLine = A:GetProfession(prof1);

    A.professions = nil;

    if ( nameGlobal ) then
        A.professions = {};

        A.professions[nameGlobal] =
        {
            name = name,
            texture = texture,
            rank = rank,
            rankModifier = rankModifier,
            spellID = spellID,
            spellName = spellName,
            spellTexture = spellTexture,
            skillLine = skillLine,
        };
    end

    nameGlobal, name, texture, rank, rankModifier, spellID, spellName, spellTexture, skillLine = A:GetProfession(prof2);

    if ( nameGlobal ) then
        if ( not A.professions ) then A.professions = {}; end

        A.professions[nameGlobal] =
        {
            name = name,
            texture = texture,
            rank = rank,
            rankModifier = rankModifier,
            spellID = spellID,
            spellName = spellName,
            spellTexture = spellTexture,
            skillLine = skillLine,
        };
    end
end

--- Get the first containers slot containing the designed item + quantity
-- @param itemID A WoW item ID
-- @param quantity The minimal quantity of item desired
-- @return bag and slot number if there is a match or nil
function A:GetSlot(itemID, quantity)
    if ( not quantity ) then quantity = 1; end

    for i=0,NUM_BAG_FRAMES do
        if ( GetBagName(i) ) then
            local slots = GetContainerNumSlots(i);

            for ii=1,slots do
                local currentItemID = GetContainerItemID(i, ii);

                if ( itemID == currentItemID ) then
                    local _, itemCount = GetContainerItemInfo(i, ii);

                    if ( itemCount >= quantity ) then
                        return i, ii;
                    end
                end
            end
        end
    end

    return nil;
end

--ITEM_SOULBOUND = "Soulbound";
--ITEM_ACCOUNTBOUND = "Account Bound";
--ITEM_BIND_ON_EQUIP = "Binds when equipped";
--ITEM_BIND_ON_PICKUP = "Binds when picked up";
--ITEM_BIND_ON_USE = "Binds when used";
--ITEM_BIND_QUEST = "Quest Item";
--ITEM_BIND_TO_ACCOUNT = "Binds to account";
--ITEM_BIND_TO_BNETACCOUNT = "Binds to Battle.net account";
--ITEM_BNETACCOUNTBOUND = "Battle.net Account Bound";

--- Check if an item is BOE
-- @param bag Bag ID
-- @param slot Bag slot ID
-- @return true or false
function A:IsNotBound(bag, slot)
    LazyTransformTooltip:SetBagItem(bag, slot);

    local lines = LazyTransformTooltip:NumLines();

    for i=1,lines do
        if ( _G["LazyTransformTooltipTextLeft"..i]:GetText() == ITEM_BIND_ON_EQUIP ) then
            LazyTransformTooltip:ClearLines();
            return 1;
        end
    end

    LazyTransformTooltip:ClearLines();
    return nil;
end

--- Check if an item is a BNET item
-- @param bag Bag ID
-- @param slot Bag slot ID
-- @return true or false
local bnItemsBoundStrings =
{
    [1] = ITEM_ACCOUNTBOUND,
    [2] = ITEM_BIND_TO_ACCOUNT,
    [3] = ITEM_BIND_TO_BNETACCOUNT,
    [4] = ITEM_BNETACCOUNTBOUND,
    [5] = ITEM_BIND_QUEST, -- Adding quest items also, just in case, it won't disenchant it but it will still get stuck
};
-- This is not safe, but the player choose to process soulbound items
-- Added this to try to protect account bound items
function A:IsBNBound(bag, slot)
    LazyTransformTooltip:SetBagItem(bag, slot);

    local lines = LazyTransformTooltip:NumLines();

    for i=1,lines do
        if ( tContains(bnItemsBoundStrings, _G["LazyTransformTooltipTextLeft"..i]:GetText()) ) then
            LazyTransformTooltip:ClearLines();
            return 1;
        end
    end

    LazyTransformTooltip:ClearLines();
    return nil;
end

--- Check if an item can be disenchant
-- @param itemType Obtained with GetItemInfo()
-- @param itemSubType Obtained with GetItemInfo()
-- @return true or false
A.itemEquipLoc =
{
    "INVTYPE_HEAD",
    "INVTYPE_NECK",
    "INVTYPE_SHOULDER",
    "INVTYPE_CHEST",
    "INVTYPE_ROBE",
    "INVTYPE_WAIST",
    "INVTYPE_LEGS",
    "INVTYPE_FEET",
    "INVTYPE_WRIST",
    "INVTYPE_HAND",
    "INVTYPE_FINGER",
    "INVTYPE_TRINKET",
    "INVTYPE_CLOAK",
    "INVTYPE_WEAPON",
    "INVTYPE_SHIELD",
    "INVTYPE_2HWEAPON",
    "INVTYPE_WEAPONMAINHAND",
    "INVTYPE_WEAPONOFFHAND",
    "INVTYPE_HOLDABLE",
    "INVTYPE_RANGED",
    "INVTYPE_RANGEDRIGHT",
};
A.itemSubTypeFishingPoles = select(-1,GetAuctionItemSubClasses(1));
function A:CanDisenchantItem(itemEquipLoc, itemSubType)
    if ( tContains(A.itemEquipLoc, itemEquipLoc) ) then
        if ( itemEquipLoc == "INVTYPE_2HWEAPON" and itemSubType == A.itemSubTypeFishingPoles ) then
            return nil;
        end

        return 1;
    end

    return nil;
end

--- Get the first herbs stack of 5 available
-- @param rank The current rank (level) of the profession
-- @param spellName Localized spell name
-- @return item ID, item icon, bag ID, slot ID or nil
function A:GetHerbSlot(rank, spellName)
    for kk,vv in pairs(A.herbs) do
        if ( rank >= vv ) then -- Got required profession level to mill that herb
            if ( not tContains(A.db.profile.milling.ignore, kk) ) then -- Herb is not ignored
                local count = GetItemCount(kk);

                if ( count >= 5 ) then -- Got at least 5 herbs
                    local itemName, _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(kk);
                    local bag, slot = A:GetSlot(kk, 5);

                    if ( bag and slot ) then
                        return kk, itemTexture, bag, slot;
                    else
                        A:Message(L["Found more than 5 %s, but no stack of 5 in your containers. %s impossible."]:format(itemName, spellName), 1);
                    end
                end
            end
        end
    end

    return nil;
end

--- Get the first ores stack of 5 available
-- @param rank The current rank (level) of the profession
-- @param spellName Localized spell name
-- @return item ID, item icon, bag ID, slot ID or nil
function A:GetOreSlot(rank, spellName)
    for kk,vv in pairs(A.ores) do
        if ( rank >= vv ) then -- Got required profession level to prospect that ore
            if ( (not tContains(A.rareOres, kk) and not tContains(A.db.profile.prospect.ignore, kk)) -- Not rare, not ignored
            or (tContains(A.rareOres, kk) and A.db.profile.prospect.rareOre and not tContains(A.db.profile.prospect.ignore, kk)) ) then -- Rare, want rare, not ignored
                local count = GetItemCount(kk);

                if ( count >= 5 ) then -- Got at least 5 ores
                    local itemName, _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(kk);
                    local bag, slot = A:GetSlot(kk, 5);

                    if ( bag and slot ) then
                        return kk, itemTexture, bag, slot;
                    else
                        A:Message(L["Found more than 5 %s, but no stack of 5 in your containers. %s impossible."]:format(itemName, spellName), 1);
                    end
                end
            end
        end
    end

    return nil;
end

--- Get the first disenchantable item available
-- @return item ID, item icon, bag ID, slot ID or nil
function A:GetDisenchantSlot()
    for i=0,NUM_BAG_FRAMES do
        if ( GetBagName(i) and A.db.profile.disenchant.bags[tostring(i)] ) then
            local slots = GetContainerNumSlots(i);

            for ii=1,slots do
                local _, _, locked = GetContainerItemInfo(i, ii);

                if ( not locked ) then
                    local itemID = GetContainerItemID(i, ii);

                    if ( itemID and not tContains(A.db.profile.disenchant.ignore, itemID) ) then
                        local _, _, itemRarity, _, _, _, itemSubType, _, itemEquipLoc, itemTexture = GetItemInfo(itemID);

                        if ( (itemRarity <= A.db.profile.disenchant.threshold and itemRarity >= 2) and A:CanDisenchantItem(itemEquipLoc, itemSubType) ) then
                            if ( A.db.profile.disenchant.soulbound ) then
                                if ( not A:IsBNBound(i, ii) ) then
                                    return itemID, itemTexture, i, ii;
                                end
                            else
                                if ( A:IsNotBound(i, ii) ) then
                                    return itemID, itemTexture, i, ii;
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    return nil;
end

--- Check if player have a merchant, bank, trade, etc, window open
-- Thanks Atlan for beying retarded and selling your farm
function A:CheckTransferWindows()
    if ( GetMerchantItemInfo(1) ) then
        A:Message(L["%s frame is open."]:format(L["Merchant"]), 1);
        return 1;
    end

    if ( A.isBankOpen ) then
        A:Message(L["%s frame is open."]:format(L["Bank"]), 1);
        return 1;
    end

    if ( A.isTradeOpen ) then
        A:Message(L["%s frame is open."]:format(L["Trade"]), 1);
        return 1;
    end

    if ( A.isVoidOpen ) then
        A:Message(L["%s frame is open."]:format(L["Void storage"]), 1);
        return 1;
    end

    if ( A.isMailOpen and A.db.profile.checkMailFrame ) then
        A:Message(L["%s frame is open."]:format(L["Mail"]), 1);
        return 1;
    end

    return nil;
end

--- Secure Button Pre click
function A:PreClick(button, clickedBy)
    if ( clickedBy == "RightButton" ) then
        -- Menu
    elseif ( clickedBy == "MiddleButton" ) then
        LibStub("AceConfigDialog-3.0"):Open("LazyTransformConfig");
    else
        if ( IsShiftKeyDown() ) then
            A:ToggleButtonLock();
        elseif ( IsControlKeyDown() ) then
            LazyTransformSecureButton:Hide();
            A.db.profile.button.hide = 1;
        else
            if ( A.inProcess or A:CheckTransferWindows() or InCombatLockdown() or UnitCastingInfo("player") or UnitChannelInfo("player") or not A.professions ) then return; end

            A.inProcess = 1;

            for k,v in pairs(A.professions) do
                if ( k == "INSCRIPTION" and A.db.profile.milling.enabled ) then
                    local itemID, itemTexture, bag, slot = A:GetHerbSlot(v.rank, v.spellName);

                    if ( itemID ) then
                        button.itemID = itemID;
                        button.itemBagID = bag;
                        button.itemSlotID = slot;
                        button.icon:SetTexture(itemTexture);
                        button:SetAttribute("type", "macro");
                        button:SetAttribute("macrotext", ("/cast %s\n/use %d %d"):format(v.spellName, bag, slot));
                        break;
                    else
                        A:ResetButton(button);
                    end
                elseif ( k == "JEWELCRAFTING" and A.db.profile.prospect.enabled ) then
                    local itemID, itemTexture, bag, slot = A:GetOreSlot(v.rank, v.spellName);

                    if ( itemID ) then
                        button.itemID = itemID;
                        button.itemBagID = bag;
                        button.itemSlotID = slot;
                        button.icon:SetTexture(itemTexture);
                        button:SetAttribute("type", "macro");
                        button:SetAttribute("macrotext", ("/cast %s\n/use %d %d"):format(v.spellName, bag, slot));
                        break;
                    else
                        A:ResetButton(button);
                    end
                elseif ( k == "ENCHANTING" and A.db.profile.disenchant.enabled ) then
                    local itemID, itemTexture, bag, slot = A:GetDisenchantSlot();

                    if ( itemID ) then
                        button.itemID = itemID;
                        button.itemBagID = bag;
                        button.itemSlotID = slot;
                        button.icon:SetTexture(itemTexture);
                        button:SetAttribute("type", "macro");
                        button:SetAttribute("macrotext", ("/stopcast\n/cast %s\n/use %d %d"):format(v.spellName, bag, slot));
                        break;
                    else
                        A:ResetButton(button);
                    end
                end
            end
        end
    end

    A.inProcess = nil;
end

--- Secure Button Post click
--function A:PostClick(button, clickedBy)
function A:PostClick(button)
    button:SetAttribute("type", nil);
    button:SetAttribute("macrotext", nil);
end

--- Reset button texture, attributes and tooltip
function A:ResetButton(button)
    button.itemID = nil;
    button.itemBagID = nil;
    button.itemSlotID = nil;
    button.icon:SetTexture("Interface\\ICONS\\Icon_PetFamily_Mechanical");
    button:SetAttribute("type", nil);
    button:SetAttribute("macrotext", nil);
end

--- Set button position
function A:SetButtonPos()
    LazyTransformSecureButton:ClearAllPoints();
    LazyTransformSecureButton:SetPoint(A.db.profile.button.point.point, A.db.profile.button.point.relativeTo, A.db.profile.button.point.relativePoint, A.db.profile.button.point.offX, A.db.profile.button.point.offY);
end

--- Lock button
function A:LockButton()
    LazyTransformSecureButton:SetMovable(nil);
    LazyTransformSecureButton:SetScript("OnDragStart", nil);
    LazyTransformSecureButton:SetScript("OnDragStop", nil);
    A.db.profile.button.lock = 1;
end

--- Unlock button, saving position
function A:UnlockButton()
    LazyTransformSecureButton:SetMovable(1);
    LazyTransformSecureButton:RegisterForDrag("LeftButton");
    LazyTransformSecureButton:SetScript("OnDragStart", LazyTransformSecureButton.StartMoving);
    LazyTransformSecureButton:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing();

        local point, relativeTo, relativePoint, offX, offY = LazyTransformSecureButton:GetPoint(1);

        A.db.profile.button.point.point = point;
        A.db.profile.button.point.relativeTo = relativeTo;
        A.db.profile.button.point.relativePoint = relativePoint;
        A.db.profile.button.point.offX = offX;
        A.db.profile.button.point.offY = offY;
    end);
    A.db.profile.button.lock = nil;
end

--- Toggle lock button
function A:ToggleButtonLock()
    if ( LazyTransformSecureButton:IsMovable() ) then
        A:LockButton();
    else
        A:UnlockButton();
    end
end

--- Button hide/show toggle
function A:ToggleButtonHideShow()
    if ( LazyTransformSecureButton:IsShown() ) then
        LazyTransformSecureButton:Hide();
        A.db.profile.button.hide = 1;
    else
        LazyTransformSecureButton:Show();
        A.db.profile.button.hide = nil;
    end
end

--- Set button on login
function A:SetButton()
    if ( A.db.profile.button.hide ) then
        LazyTransformSecureButton:Hide();
    else
        LazyTransformSecureButton:Show();
    end

    if ( A.db.profile.button.lock ) then
        A:LockButton();
    else
        A:UnlockButton();
    end

    A:SetButtonPos();
end

--- Button OnShow callback
function A:OnShow()
    A:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED");
end

--- Button OnHide callback
function A:OnHide()
    A:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED");
end

--- Set the button tooltip when no itemID is defined
function A:DefaultTooltip()
    GameTooltip:AddDoubleLine(A.color["WHITE"]..L["Lazy Transform"], A.color["GREEN"].." v"..A.version);
    GameTooltip:AddLine(" ");
    GameTooltip:AddLine(L["Professions available:"]);

    for k,v in pairs(A.professions) do
        GameTooltip:AddDoubleLine(("|T%s:16|t %s"):format(v.texture, v.name), ("|T%s:16|t %s"):format(v.spellTexture, v.spellName));
    end

    GameTooltip:AddLine(" ");
    GameTooltip:AddLine(L["|cFFC79C6ELeft-Click: |cFF33FF99Process.\n|cFFC79C6EShift+Left-Click: |cFF33FF99Toggle button lock.\n|cFFC79C6EControl+Left-Click: |cFF33FF99Hide button.\n|cFFC79C6ERight-Click: |cFF33FF99Display quick access menu.\n|cFFC79C6EMiddle-Click: |cFF33FF99Open configuration panel."]);
end

-- Set the tooltip with the item currently being destroyed
function A:SetTooltip(frame)
    local x = frame:GetRight();

    if ( x >= ( GetScreenWidth() / 2 ) ) then
        GameTooltip:SetOwner(frame, "ANCHOR_LEFT");
    else
        GameTooltip:SetOwner(frame, "ANCHOR_RIGHT");
    end

    if ( frame.itemID and frame.itemBagID and frame.itemSlotID ) then
        GameTooltip:SetBagItem(frame.itemBagID, frame.itemSlotID);
    else
        A:DefaultTooltip();
    end

    GameTooltip:Show();
end

--- Set bindings
function A:SetBindings()
    if ( InCombatLockdown() ) then
        A.delayedBindings = 1;
    else
        local key1, key2 = GetBindingKey("LAZYTRANSFORMBIND");

        if ( key1 ) then
            SetOverrideBindingClick(LazyTransformSecureButton, 1, key1, "LazyTransformSecureButton");
            LazyTransformSecureButtonHotKey:SetText(key1);
        end

        if ( key2 ) then
            SetOverrideBindingClick(LazyTransformSecureButton, 1, key2, "LazyTransformSecureButton");

            if ( not key1 ) then
                LazyTransformSecureButtonHotKey:SetText(key2);
            end
        end
    end
end

-- ********************************************************************************
-- Callbacks
-- ********************************************************************************

--- Callback function for event PLAYER_ENTERING_WORLD
function A:PLAYER_ENTERING_WORLD()
    A:GetProfessions();

    if ( A.professions ) then
        A:LDBInit();
        LazyTransformSecureButton.icon:SetTexture("Interface\\ICONS\\Icon_PetFamily_Mechanical");
        A:SetButton();
        A:SetBindings();
        A:BuildItemsCache();
    else
        A:Disable();
    end

    A:UnregisterEvent("PLAYER_ENTERING_WORLD");
end

--- Callback function for event PLAYER_REGEN_ENABLED
function A:PLAYER_REGEN_ENABLED()
    if ( A.delayedBindings ) then
        A:SetBindings();
        A.delayedBindings = nil;
    end
end

--- Callback for event BANKFRAME_OPENED
function A:BANKFRAME_OPENED()
    A.isBankOpen = 1;
end

--- Callback for event BANKFRAME_CLOSED
function A:BANKFRAME_CLOSED()
    A.isBankOpen = nil;
end

--- Callback for event TRADE_SHOW
function A:TRADE_SHOW()
    A.isTradeOpen = 1;
end

--- Callback for event TRADE_CLOSED
function A:TRADE_CLOSED()
    A.isTradeOpen = nil;
end

--- Hook an OnShow script when loading void storage addon
function A:ADDON_LOADED(event, addon)
    if ( addon == "Blizzard_VoidStorageUI" ) then
        VoidStorageFrame:HookScript("OnShow", function() A.isVoidOpen = 1; end);
        VoidStorageFrame:HookScript("OnHide", function() A.isVoidOpen = nil; end);
    end
end

--- Callback for event MAIL_SHOW
function A:MAIL_SHOW()
    A.isMailOpen = 1;
end

--- Callback for event MAIL_CLOSED
function A:MAIL_CLOSED()
    A.isMailOpen = nil;
end

--- Callback for event UNIT_SPELLCAST_SUCCEEDED
function A:UNIT_SPELLCAST_SUCCEEDED(_, unit, _, _, _, spellID)
    if ( unit == "player" and spellID == 13262 ) then
        A:ResetButton(LazyTransformSecureButton);
    end
end

-- ********************************************************************************
-- Main
-- ********************************************************************************

--- AceAddon callback
-- Called after the addon is fully loaded
function A:OnInitialize()
    -- Config db
    A.db = LibStub("AceDB-3.0"):New("LazyTransformDB", A.defaultDB);

    -- Configuration panel
    LibStub("AceConfig-3.0"):RegisterOptionsTable("LazyTransformConfig", A.ConfigurationPanel);
    LibStub("AceConfigDialog-3.0"):SetDefaultSize("LazyTransformConfig", 800, 500);
    A.configFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("LazyTransformConfig", L["Lazy Transform"]);
end

--- AceAddon callback
-- Called during the PLAYER_LOGIN event
function A:OnEnable()
    -- Events
    A:RegisterEvent("PLAYER_ENTERING_WORLD");
    A:RegisterEvent("PLAYER_REGEN_ENABLED");
    A:RegisterEvent("UPDATE_BINDINGS", "SetBindings");
    A:RegisterEvent("BANKFRAME_OPENED");
    A:RegisterEvent("BANKFRAME_CLOSED");
    A:RegisterEvent("TRADE_SHOW");
    A:RegisterEvent("TRADE_CLOSED");
    A:RegisterEvent("ADDON_LOADED");
    A:RegisterEvent("MAIL_SHOW");
    A:RegisterEvent("MAIL_CLOSED");

    -- Slash command
    A:RegisterChatCommand("lazytransform", "SlashCommand");
    A:RegisterChatCommand("lt", "SlashCommand");
end

--- AceAddon callback
-- Called when the addon is manually disabled
function A:OnDisable()
    -- Slash command
    A:UnregisterChatCommand("lazytransform");
    A:UnregisterChatCommand("lt");

    -- Events
    A:UnregisterEvent("PLAYER_ENTERING_WORLD");
    A:UnregisterEvent("PLAYER_REGEN_ENABLED");
    A:UnregisterEvent("UPDATE_BINDINGS");
    A:UnregisterEvent("BANKFRAME_OPENED");
    A:UnregisterEvent("BANKFRAME_CLOSED");
    A:UnregisterEvent("TRADE_SHOW");
    A:UnregisterEvent("TRADE_CLOSED");
    A:UnregisterEvent("ADDON_LOADED");
    A:UnregisterEvent("MAIL_SHOW");
    A:UnregisterEvent("MAIL_CLOSED");
end
